GraalVM + Micronautでリフレクションを使いたい
Introduction
GraalVMは、多言語対応の仮想マシンとプラットフォームです。
Java/JVM言語/Node/LLVM言語をサポートしており、
Javaプログラムをネイティブコンパイルして高速動作が可能です。
※このへん参照
GraalVMではAOT(Ahead Of Tim)コンパイルと呼ばれるコンパイルができます。
これはJavaのコードをNative Imageと呼ばれる
実行可能形式にコンパイルできるのですが、
リフレクションをつかっている場合にそのままだと
実行できないことがあります。
本稿ではMicronaut + GraalVMで
リフレクションを使いたいケースについて試してみました。
Environment
- MacBook Pro (13-inch, M1, 2020)
- OS : MacOS 12.4
Setup
MicronautやGraalVMのインストールにはSdkManを使います。
インストールしてない場合はインストールしましょう。
% curl -s "https://get.sdkman.io" | bash
GraalVMとMicronautをインストールします。
% sdk update % sdk install java 22.3.r17-grl % sdk install micronaut ・・・ % java --version openjdk 17.0.5 2022-10-18 OpenJDK Runtime Environment GraalVM CE 22.3.0 (build 17.0.5+8-jvmci-22.3-b08) OpenJDK 64-Bit Server VM GraalVM CE 22.3.0 (build 17.0.5+8-jvmci-22.3-b08, mixed mode, sharing) % mn --version Micronaut Version: 3.7.3
インストールOKです。
次にアプリを実装していきます。
Try
local_grという名前でGraalVM用アプリの雛形を作成します。
% mn create-app example.local_gr \ --features=graalvm,serialization-jackson \ --build=gradle --lang=java
次に、src/main/exampleにリフレクションを使って呼び出すクラスを作成します。
シンプルなメソッドを1つ持っているだけのクラスです。
package example; public class ReflectionService { private void sayHello() { System.out.println("hello"); } }
src/main/exampleにHelloController.javaを作成。
ここでReflectionServiceのメソッドをリフレクションで呼び出します。
package example; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Optional; @Controller("/hello") public class HelloController { @Get("/reflection") public String sayHello() { ReflectionService service = new ReflectionService(); Class<? extends ReflectionService> clazz = service.getClass(); try { Method printHoge = clazz.getDeclaredMethod("sayHello"); printHoge.setAccessible(true); printHoge.invoke(service, (Object[])null); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); throw new RuntimeException(); } return "sayHello"; } }
ネイティブコンパイルします。
3〜4分くらいかかる。
% ./gradlew nativeCompile
コンパイルできたらネイティブイメージを起動しましょう。
% ./build/native/nativeCompile/local_gr __ __ _ _ | \/ (_) ___ _ __ ___ _ __ __ _ _ _| |_ | |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __| | | | | | (__| | | (_) | | | | (_| | |_| | |_ |_| |_|_|\___|_| \___/|_| |_|\__,_|\__,_|\__| Micronaut (v3.7.3) 16:07:17.267 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 355ms. Server Running: http://localhost:8080
アクセスしてみます。
これは問題なく動作します。
% curl http://localhost:8080/hello/reflection
この場合はリフレクションを使っていても、コンパイル時の静的解析で検出可能だからです。
しかし、ControllerのsayHelloメソッドを次のようにすると、
クエリパラメータによって呼び出される関数が
動的に変化するため実行できません。
@Get("/reflection") public String sayHello(Optional<String> name) { ReflectionService service = new ReflectionService(); Class<? extends ReflectionService> clazz = service.getClass(); try { Method printHoge = clazz.getDeclaredMethod(name.orElseThrow()); printHoge.setAccessible(true); printHoge.invoke(service, (Object[])null); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); throw new RuntimeException(); } return "sayHello"; }
スタックトレースをみると、NoSuchMethodExceptionが発生してエラーになっています。
% curl http://localhost:8080/hello/reflection?name=sayHello {"message":"Internal Server Error","_links":{"self":[{"href":"/hello/reflection?name=sayHello","templated":false}]},"_embedded":{"errors":[{"message":"Internal Server Error: null"}]}}
reflection-config.jsonで設定
動的に呼び出すクラス・メソッドが変化する場合、
設定ファイルで事前にどういった定義が必要が記述しておきます。
src/main/resources/META-INF/native-image/example/local_grディレクトリに
reflection-config.jsonを下記のように作成します。
[ { "name" : "example.ReflectionService", "methods" : [ { "name" : "sayHello", "parameterTypes" : [ ] }] } ]
そして、build.gradleに次の定義を追加します。
graalvmNative { binaries { main { buildArgs.add("-H:ReflectionConfigurationFiles=/path/your/local_gr/src/main/resources/META-INF/native-image/example/local_gr/reflection-config.json") } } }
ネイティブコンパイル時の引数に
さきほどのreflection-config.jsonを指定してあげます。
再度ネイティブコンパイルして起動。
今度はちゃんとメソッドが呼び出しできています。
% ./gradlew nativeCompile % ./build/native/nativeCompile/local_gr #今度は動く % curl http://localhost:8080/hello/reflection?name=sayHello
Tracing Agentでreflection-configの自動生成
今回の例のように単純なものであればいいのですが、
リフレクションを多用している場合にいちいち
設定ファイルを書いていくのは現実的ではないです。
そういった場合、Tracing Agentというツールを使って
設定ファイルの自動生成を行います。
このツールは、VMの実行時(ネイティブイメージでない状態でアプリ実行しているとき)に
アプリの動作をトレースすることにより、JSONファイルを生成するツールです。
具体的には、単体テスト実行時にその挙動をトレースして定義ファイルを生成します。
では、build.gradleでテスト実行時にTracing Agentを使うように指定します。
test { jvmArgs "-agentlib:native-image-agent=config-merge-dir=src/main/resources/META-INF/native-image/auto/" }
元からあるテストクラス(test/java/example/Local_grTest.java)に、
ReflectionServiceのメソッドを実行するテストを追加します。
@Test void reflectTest() { Class<? extends ReflectionService> clazz = ReflectionService.class; try { ReflectionService service = clazz.newInstance(); Method printHoge = clazz.getDeclaredMethod("sayHello"); printHoge.setAccessible(true); printHoge.invoke(service, (Object[])null); } catch (Exception e) { e.printStackTrace(); } }
テストを実行。
./gradlew test
テストが終わると、build.gradleで指定したパスに
reflection-config.jsonなどの設定ファイルもろもろが生成されてます。
・・・ { "name":"example.ReflectionService", "methods":[ {"name":"<init>","parameterTypes":[] }, {"name":"sayHello","parameterTypes":[] } ] }, ・・・
そしてネイティブコンパイル・起動すれば、生成したファイル郡がロードされて、
先程とおなじく動きます。
※build.gradleのgraalvmNativeセクションは削除してOK
Summary
今回はGraalVM + Micronautでリフレクションの動作を確認してみました。
GraalVMは今後Javaでパフォーマンスを求めるなら避けては通れないと思っているので、
しっかりおさえておきたいところです。